﻿using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using PlottingBasics.Functions;
using System.Threading;
using PlottingBasics.PlotManipulations;

namespace ProbabilityNot
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            LayoutRoot.DataContext = this;

            BackgroundWidth = 0.3;
            BackgroundMean = 0.8;
                
            ProbabilityPercent = 20.0;
            Bins = 200;

            CheckForUpdatesButton.Visibility = Application.Current.IsRunningOutOfBrowser ? Visibility.Visible : Visibility.Collapsed;

            ///
            /// Our GUI interface
            /// 
#if true
            /// Use this for good performance
            LocalDataPointsGaussian = new DistributionCopier(this, DataPointsGaussianProperty, 0.01);
            LocalDataPointsGaussian.DelayTicks = 250;
            LocalDataPointsPoisson = new DistributionCopier(this, DataPointsPoissonProperty);
            LocalCutDataPointsGaussian = new DistributionCopier(this, CutPointsGausianProperty, 0.01);
            LocalCutDataPointsGaussian.DelayTicks = 250;
            LocalCutDataPointsPoisson = new DistributionCopier(this, CutPointsPoissonProperty);
#else
            /// This performance sucks!
            LocalDataPointsGaussian = new DistributionCopier(this, DataPointsGaussianProperty);
            LocalDataPointsPoisson = new DistributionCopier(this, DataPointsPoissonProperty);
            LocalCutDataPointsGaussian = new DistributionCopier(this, CutPointsGausianProperty);
            LocalCutDataPointsPoisson = new DistributionCopier(this, CutPointsPoissonProperty);
#endif
            ///
            /// Once we are configured and have shown up, we need to update all the display to show everything.
            /// 

            Loaded += (s, args) =>
            {
                _disableUpdates = false;
                UpdateAxis();
            };
        }

        /// <summary>
        /// Prevent GUI updates when we start up - that way we come up faster.
        /// </summary>
        private bool _disableUpdates = true;

        #region Local Copies Of Distributions And GUI Update Code

        /// <summary>
        /// Holds the thread-safe versions of everything.
        /// </summary>
        private DistributionCopier LocalDataPointsGaussian { get; set; }
        private DistributionCopier LocalDataPointsPoisson { get; set; }
        private DistributionCopier LocalCutDataPointsGaussian { get; set; }
        private DistributionCopier LocalCutDataPointsPoisson { get; set; }

        #endregion

        #region Distribution And Probability Code
        /// <summary>
        /// The axis we are using
        /// </summary>
        PlottingBasics.Axis.LinearAxis _axis;

        /// <summary>
        /// Someone changed the number of bins - we will need to re-generate everything! This needs to be run on the interface thread.
        /// </summary>
        /// <returns></returns>
        private void UpdateAxis()
        {
            if (_disableUpdates)
            {
                return;
            }

            ///
            /// Some sanity checks!
            /// 

            if (Bins == 0
                || BackgroundWidth == 0)
            {
                return;
            }

            ///
            /// First, determine what the min and max are when it comes to the # of bins. Since
            /// we are doing probability here, we just do it when it "goes to zero".
            /// 

            var gf = Gaussian.Gauss(BackgroundMean, BackgroundWidth);
            var gausTails = from val in Enumerable.Range(1, 100)
                            select new {
                                f = gf(BackgroundMean * BackgroundWidth * val),
                                sig = val
                            };

            var gGirstSmall = from g in gausTails
                             where g.f < 0.05
                             select g;
            var gausMaxSigR = gGirstSmall.FirstOrDefault();
            int gausMaxSig = 1;
            if (gausMaxSigR != null)
            {
                gausMaxSig = gausMaxSigR.sig;
            }

            var pf = Poisson.Pois(BackgroundMean + BackgroundWidth * gausMaxSig);
            var poisTails = from val in Enumerable.Range(gausMaxSig, 100)
                            select new
                            {
                                f = pf((int) (BackgroundMean + BackgroundWidth * val)),
                                sig = val
                            };
            var pFirstSmall = from p in poisTails
                             where p.f < 0.05
                             select p;
            var poisMaxSigR = pFirstSmall.FirstOrDefault();
            int poisMaxSig = gausMaxSig;
            if (poisMaxSigR != null)
            {
                poisMaxSig = poisMaxSigR.sig;
            }

            double xmin = BackgroundMean - BackgroundWidth * poisMaxSig;
            if (xmin < 0)
            {
                xmin = 0.0;
            }
            else
            {
                xmin = (int)xmin;
            }

            double xmax = BackgroundMean + BackgroundWidth * poisMaxSig;
            xmax = (int)(xmax + 1.0); // It isn't about rounding - but making sure we include the last point!
            xmax = xmax + 1.0; // Make sure we are one beyond when we get small - sometimes the Poisson drops quickly).

            XMin = xmin;
            XMax = xmax;

            ///
            /// Next, calculate the # of bins. This is defined by making sure we have enough bins in the central gaussian hump (50 of them there).
            /// 

            double gMin = BackgroundMean - gausMaxSig * BackgroundWidth;
            double gMax = BackgroundMean + gausMaxSig * BackgroundWidth;
            gMin = gMin < XMin ? XMin : gMin;
            gMax = gMax > XMax ? XMax : gMax;
            double deltaPerBin = (gMax - gMin) / 75.0;
            Bins = (int)((XMax - XMin) / deltaPerBin);

            ///
            /// Some protection
            /// 

            if (Bins > 2000)
            {
                Bins = 2000;
            }

            ///
            /// Create the axis...
            /// 

            _axis = new PlottingBasics.Axis.LinearAxis(Bins, XMin, XMax);

            ///
            /// Chain the updates
            /// 

            BeginUpdatePoisson();
            UpdateGausian();
        }

        /// <summary>
        /// Update the gaussian guy - this must be run on the interface thread.
        /// because it is so fast.
        /// </summary>
        private void UpdateGausian()
        {
            if (_disableUpdates)
            {
                return;
            }

            LocalDataPointsGaussian.Local = ApplyFunction.Apply(_axis, Gaussian.Gauss(BackgroundMean, BackgroundWidth)).ToArray();
            UpdateGaussianCut();
        }

        /// <summary>
        /// Calculates the curve that shows where the cut is. Must be called on the GUI thread.
        /// </summary>
        private void UpdateGaussianCut()
        {
            UpdatePercentCurve(LocalDataPointsGaussian, ProbabilityPercent, (points, cutval) => LocalCutDataPointsGaussian.Local = points);
        }

        /// <summary>
        /// Run through the sampling for a poisson distribution. Can be called on background or GUI thread.
        /// </summary>
        private void UpdatePoisson(double backMean, double backWidth, double cutPercent, double xmin, double xmax)
        {
            if (_disableUpdates)
            {
                return;
            }

            ///
            /// First, sample the gausian - this will give central values
            /// for the poisson distribution
            /// 

            var gs = Gaussian.Gauss(backMean, backWidth);
            Random ran = new Random();

            var sampleWeights = from cv in Enumerable.Range(1, Bins * 20)
                                let meanVal = ran.NextDouble() * xmax
                                select new
                                {
                                    MeanValue = meanVal,
                                    weight = gs(meanVal),
                                };

            ///
            /// Now, do the same thing for the poisson.
            /// 

            var pDist = from expectedEvents in sampleWeights
                        let pos = Poisson.Pois(expectedEvents.MeanValue)
                        from nEvents in Enumerable.Range((int)xmin, (int)xmax + 1)
                        select new
                        {
                            numEvents = nEvents,
                            weight = expectedEvents.weight * pos(nEvents)
                        };

            ///
            /// Great! Last thing for us to do is organize by # of events and sum the weights
            /// and normalize! Force the calculation at this point - we've built up all the expensive stuff
            /// now.
            /// 

            var distForEvents = (from ev in pDist
                                 group ev.weight by ev.numEvents into weightList
                                 select new Point(weightList.Key, weightList.Sum())).ToArray();

            double total = distForEvents.Sum(a => a.Y); // Bin width is 1 so we don't have to multiply.
            LocalDataPointsPoisson.Local = (from d in distForEvents
                                     select new Point(d.X, d.Y / total)).ToArray();

            ///
            /// Update the cut
            /// 

            UpdatePoissonCut(cutPercent);
        }

        /// <summary>
        /// Recalcs the poisson curve. Can be called on GUI or background thread.
        /// </summary>
        private void UpdatePoissonCut(double cutPercent)
        {
            UpdatePercentCurve(LocalDataPointsPoisson, cutPercent, (points, cutval) =>
            {
                LocalCutDataPointsPoisson.Local = points;
                Dispatcher.BeginInvoke(() => NumberEventsPoisson = cutval);
            });
        }

        /// <summary>
        /// Fires off the update, cna be run only from GUI thread. Returns before things are done.
        /// </summary>
        private void BeginUpdatePoisson()
        {
            double backMean = BackgroundMean;
            double backWidth = BackgroundWidth;
            double cutPercent = ProbabilityPercent;
            double xmin = XMin;
            double xmax = XMax;
            ThreadPool.QueueUserWorkItem(o => UpdatePoisson(backMean, backWidth, cutPercent, xmin, xmax));
        }

        /// <summary>
        /// Update the curve that tells us where to draw the percent number.
        /// We expect to be called on a background thread.
        /// </summary>
        /// <param name="percentCut">The prob at the right end of the curve</param>
        private void UpdatePercentCurve(Point[] pointsArray, double percentCut, Action<Point[], double> saveResult)
        {
            double xTurnOn;
            var finalCurve = pointsArray.FlattenCurveLessThanIntegralCut(1.0 - (percentCut / 100.0), out xTurnOn).ToArray();

            ///
            /// Save it out!
            /// 

            saveResult(finalCurve, xTurnOn);
        }

        #endregion

        #region Cuts Changed Specific Code
        /// <summary>
        /// The % cut has changed, update everything! Called on GUI thread
        /// </summary>
        /// <param name="propchg"></param>
        /// <returns></returns>
        private void ProbabilityPercentChanged(DependencyPropertyChangedEventArgs propchg)
        {
            if (!_ProbabilityPercentSetLocally)
            {
                UpdateGaussianCut();
                UpdatePoissonCut(ProbabilityPercent);
            }
        }

        /// <summary>
        /// The number of events changed - this happens in concert with a % change, or on its own.
        /// So we have to watch out for "loops".
        /// </summary>
        /// <param name="propchg"></param>
        /// <returns></returns>
        private void NumberEventsPoissonChanged(DependencyPropertyChangedEventArgs propchg)
        {
            if (!_NumberEventsPoissonSetLocally)
            {
                double frac = LocalDataPointsPoisson.Local.FractionProbabilityBeforeX(NumberEventsPoisson);
                ProbabilityPercent = (1.0 - frac) * 100.0;
                UpdateGaussianCut();
                UpdatePoissonCut(ProbabilityPercent);
            }
        }
        #endregion

        #region Basic Parameter Properties
        /// <summary>
        /// Get/Set axis max.
        /// </summary>
        private double XMin { get; set; }

        /// <summary>
        /// Get/Set axis max.
        /// </summary>
        private double XMax {get; set;}

        /// <summary>
        /// Get/Set the number of bins we shuld have on our plots.
        /// </summary>
        public int Bins { get; set; }

        /// <summary>
        /// Get/Set the mean of the background
        /// </summary>
        public double BackgroundMean
        {
            get { return (double)GetValue(BackgroundMeanProperty); }
            set { SetValue(BackgroundMeanProperty, value); }
        }

        // Using a DependencyProperty as the backing store for BackgroundMean.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty BackgroundMeanProperty =
            DependencyProperty.Register("BackgroundMean", typeof(double), typeof(MainPage), new PropertyMetadata(0.0, (source, pc) => (source as MainPage).UpdateAxis()));

        /// <summary>
        /// Get/Set the width of the background.
        /// </summary>
        public double BackgroundWidth
        {
            get { return (double)GetValue(BackgroundWidthProperty); }
            set { SetValue(BackgroundWidthProperty, value); }
        }

        // Using a DependencyProperty as the backing store for BackgroundWidth.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty BackgroundWidthProperty =
            DependencyProperty.Register("BackgroundWidth", typeof(double), typeof(MainPage), new PropertyMetadata(0.0, (source, pc) => (source as MainPage).UpdateAxis()));
        #endregion

        #region Results Dependency Parameters
        /// <summary>
        /// Turns true when this is being set by local code. Prevents feedback loops when tested!
        /// </summary>
        private bool _ProbabilityPercentSetLocally = false;

        /// <summary>
        /// The amount of probability on the right... Expressed in percent.
        /// </summary>
        public double ProbabilityPercent
        {
            get { return (double)GetValue(ProbabilityPercentProperty); }
            set {
                bool temp = _ProbabilityPercentSetLocally;
                _ProbabilityPercentSetLocally = true;
                SetValue(ProbabilityPercentProperty, value);
                _ProbabilityPercentSetLocally = false;
            }
        }

        // Using a DependencyProperty as the backing store for PercentValue.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ProbabilityPercentProperty =
            DependencyProperty.Register("PercentValue", typeof(double), typeof(MainPage), new PropertyMetadata(20.0, (source, propchg) => (source as MainPage).ProbabilityPercentChanged(propchg)));

        /// <summary>
        /// Set to true when the # events (poisson) is getting set via the setter method. GUI changes go directly
        /// to the dependency property.
        /// </summary>
        private bool _NumberEventsPoissonSetLocally = false;

        /// <summary>
        /// Get/Set the number of events we find in a poissson distribution to the right of the % cut.
        /// </summary>
        public double NumberEventsPoisson
        {
            get { return (double)GetValue(NumberEventsPoissonProperty); }
            set {
                bool temp = _NumberEventsPoissonSetLocally;
                _NumberEventsPoissonSetLocally = true;
                SetValue(NumberEventsPoissonProperty, value);
                _NumberEventsPoissonSetLocally = temp;
            }
        }

        // Using a DependencyProperty as the backing store for NumberEventsPoisson.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty NumberEventsPoissonProperty =
            DependencyProperty.Register("NumberEventsPoisson", typeof(double), typeof(MainPage), new PropertyMetadata(0.0, (source, propchg) => (source as MainPage).NumberEventsPoissonChanged(propchg)));

        #endregion

        #region Plotting Distribution Point Series
        /// <summary>
        /// The distribution for the poisson simulation.
        /// </summary>
        public ObservableCollection<Point> DataPointsPoisson
        {
            get { return (ObservableCollection<Point>)GetValue(DataPointsPoissonProperty); }
            set { SetValue(DataPointsPoissonProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PoissonDistro.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataPointsPoissonProperty =
            DependencyProperty.Register("DataPointsPoisson", typeof(ObservableCollection<Point>), typeof(MainPage), new PropertyMetadata(null));

        /// <summary>
        /// Get/Set the Gaussian datapoints
        /// </summary>
        public ObservableCollection<Point> DataPointsGaussian
        {
            get { return (ObservableCollection<Point>)GetValue(DataPointsGaussianProperty); }
            set { SetValue(DataPointsGaussianProperty, value); }
        }

        // Using a DependencyProperty as the backing store for DataPoints.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataPointsGaussianProperty =
            DependencyProperty.Register("DataPointsGaussian", typeof(ObservableCollection<Point>), typeof(MainPage), new PropertyMetadata(null));

        /// <summary>
        /// Get/Set the list of points for the gaussian (they mimic the DataPoints), but are only non-zero for the
        /// probability.
        /// </summary>
        public ObservableCollection<Point> CutPointsGaussian
        {
            get { return (ObservableCollection<Point>)GetValue(CutPointsGausianProperty); }
            set { SetValue(CutPointsGausianProperty, value); }
        }

        // Using a DependencyProperty as the backing store for CutPoints.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CutPointsGausianProperty =
            DependencyProperty.Register("CutPointsGaussian", typeof(ObservableCollection<Point>), typeof(MainPage), new PropertyMetadata(null));

        /// <summary>
        /// Get?Set the plot of points that are the poisson distribution to the right of the % cut.
        /// </summary>
        public ObservableCollection<Point> CutPointsPoisson
        {
            get { return (ObservableCollection<Point>)GetValue(CutPointsPoissonProperty); }
            set { SetValue(CutPointsPoissonProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PoissonPropCutPoints.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CutPointsPoissonProperty =
            DependencyProperty.Register("CutPointsPoisson", typeof(ObservableCollection<Point>), typeof(MainPage), new PropertyMetadata(null));

        #endregion

        #region Silverlight Update App Code
        ///
        /// User would like to know if there is an update or not...
        /// 
        private void CheckForUpdates(object sender, System.Windows.RoutedEventArgs e)
        {
            CheckForUpdatesButton.IsEnabled = false;
            CheckForUpdatesButton.Content = "Checking...";

            Application.Current.CheckAndDownloadUpdateCompleted += new CheckAndDownloadUpdateCompletedEventHandler(Current_CheckAndDownloadUpdateCompleted);
            Application.Current.CheckAndDownloadUpdateAsync();
        }

        /// <summary>
        /// If the update has arrived...
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Current_CheckAndDownloadUpdateCompleted(object sender, CheckAndDownloadUpdateCompletedEventArgs e)
        {
            CheckForUpdatesButton.Content = "Check For Updates";
            CheckForUpdatesButton.IsEnabled = true;

            if (e.Error != null
                && e.Error is PlatformNotSupportedException)
            {
                MessageBox.Show("Update availible for 'Probability Not', however you must first upgrade to a newer version of Silverlight!");
            }
            else if (e.Error == null && e.UpdateAvailable)
            {
                MessageBox.Show("Hey! An update for 'Probability Not' is availible. Please restart to use it");
            }
        }

        #endregion
    }
}
